<template>
	<view class="image-cropper" @wheel="cropper.mousewheel">
		<canvas v-if="use2d" type="2d" id="imgCanvas" class="img-canvas" :style="{
			width: `${canvansWidth}px`,
			height: `${canvansHeight}px`
		}"></canvas>
		<canvas v-else id="imgCanvas" canvas-id="imgCanvas" class="img-canvas" :style="{
			width: `${canvansWidth}px`,
			height: `${canvansHeight}px`
		}"></canvas>
		<view class="pic-preview" :change:init="cropper.initObserver" :init="initData" @touchstart="cropper.touchstart" @touchmove="cropper.touchmove" @touchend="cropper.touchend">
			<image v-if="imgSrc" id="crop-image" class="crop-image" :style="cropper.imageStyles" :src="imgSrc" webp></image>
			<view v-for="(item, index) in maskList" :key="item.id" :id="item.id" class="crop-mask-block" :style="cropper.maskStylesList[index]"></view>
			<view v-if="showBorder" id="crop-border" class="crop-border" :style="cropper.borderStyles"></view>
			<view v-if="radius > 0" id="crop-circle-box" class="crop-circle-box" :style="cropper.circleBoxStyles">
				<view class="crop-circle" id="crop-circle" :style="cropper.circleStyles"></view>
			</view>
			<block v-if="showGrid">
				<view v-for="(item, index) in gridList" :key="item.id" :id="item.id" class="crop-grid" :style="cropper.gridStylesList[index]"></view>
			</block>
			<block v-if="showAngle">
				<view v-for="(item, index) in angleList" :key="item.id" :id="item.id" class="crop-angle" :style="cropper.angleStylesList[index]">
					<view :style="[{
						width: `${angleSize}px`,
						height: `${angleSize}px`
					}]"></view>
				</view>
			</block>
		</view>
		<view
        class="fixed-bottom">
			<view v-if="rotatable && !!imgSrc" class="rotate-icon" @click="cropper.rotateImage"></view>
			<view v-if="!choosable" class="choose-btn" @click="cropClick">确定</view>
			<block v-else-if="!!imgSrc">
				<view
        style="display: inline-block;float: right;margin-right: 20rpx;"
        >
          <view class="rechoose"
                style="margin-right: 20rpx;"
                @click="chooseImage">重选</view>
          <button class="button" size="mini" @click="cropClick">确定</button>
        </view>
			</block>
			<view v-else class="choose-btn" @click="chooseImage">选择图片</view>
		</view>
	</view>
</template>

<!-- #ifdef APP-VUE || H5 -->
<script module="cropper" lang="renderjs">
	import cropper from './qf-image-cropper.render.js';
	export default {
		mixins: [ cropper ]
	}
</script>
<!-- #endif -->
<!-- #ifdef MP-WEIXIN || MP-QQ -->
<script module="cropper" lang="wxs" src="../../qf-image-cropper.wxs"></script>
<!-- #endif -->
<script>
	/** 裁剪区域最大宽高所占屏幕宽度百分比 */
	const AREA_SIZE = 75;
	/** 图片默认宽高 */
	const IMG_SIZE = 300;
 
	export default {
		name:"qf-image-cropper",
		// #ifdef MP-WEIXIN
		options: {
			// 表示启用样式隔离，在自定义组件内外，使用 class 指定的样式将不会相互影响
			styleIsolation: "isolated"
		},
		// #endif
		props: {
			/** 图片资源地址 */
			src: {
				type: String,
				default: ''
			},
			/** 裁剪宽度，有些平台或设备对于canvas的尺寸有限制，过大可能会导致无法正常绘制 */
			width: {
				type: Number,
				default: IMG_SIZE
			},
			/** 裁剪高度，有些平台或设备对于canvas的尺寸有限制，过大可能会导致无法正常绘制 */
			height: {
				type: Number,
				default: IMG_SIZE
			},
			/** 是否绘制裁剪区域边框 */
			showBorder: {
				type: Boolean,
				default: true
			},
			/** 是否绘制裁剪区域网格参考线 */
			showGrid: {
				type: Boolean,
				default: true
			},
			/** 是否展示四个支持伸缩的角 */
			showAngle: {
				type: Boolean,
				default: true
			},
			/** 裁剪区域最小缩放倍数 */
			areaScale: {
				type: Number,
				default: 0.3
			},
			/** 图片最大缩放倍数 */
			maxScale: {
				type: Number,
				default: 5
			},
			/** 是否有回弹效果：拖动时可以拖出边界，释放时会弹回边界 */
			bounce: {
				type: Boolean,
				default: true
			},
			/** 是否支持翻转 */
			rotatable: {
				type: Boolean,
				default: true
			},
			/** 是否支持从本地选择素材 */
			choosable: {
				type: Boolean,
				default: true
			},
			/** 四个角尺寸，单位px */
			angleSize: {
				type: Number,
				default: 20
			},
			/** 四个角边框宽度，单位px */
			angleBorderWidth: {
				type: Number,
				default: 2
			},
			/** 裁剪图片圆角半径，单位px */
			radius: {
				type: Number,
				default: 0
			},
			/** 生成文件的类型，只支持 'jpg' 或 'png'。默认为 'png' */
			fileType: {
				type: String,
				default: 'png'
			},
			/**
			 * 图片从绘制到生成所需时间，单位ms
			 * 微信小程序平台使用 `Canvas 2D` 绘制时有效
			 * 如绘制大图或出现裁剪图片空白等情况应适当调大该值，因 `Canvas 2d` 采用同步绘制，需自己把控绘制完成时间
			 */
			delay: {
				type: Number,
				default: 1000
			},
			// #ifdef H5
			/** 
			 * 页面是否是原生标题栏
			 * H5平台当 showAngle 为 true 时，使用插件的页面在 `page.json` 中配置了 "navigationStyle": "custom" 时，必须将此值设为 false ，否则四个可拉伸角的触发位置会有偏差。
			 * 注：因H5平台的窗口高度是包含标题栏的，而屏幕触摸点的坐标是不包含的
			 */
			navigation: {
				type: Boolean,
				default: true
			}
			// #endif
		},
		emits: ["crop"],
		data() {
			return {
				// 用不同 id 使 v-for key 不重复
				maskList: [
					{ id: 'crop-mask-block-1' },
					{ id: 'crop-mask-block-2' },
					{ id: 'crop-mask-block-3' },
					{ id: 'crop-mask-block-4' },
				],
				gridList: [
					{ id: 'crop-grid-1' },
					{ id: 'crop-grid-2' },
					{ id: 'crop-grid-3' },
					{ id: 'crop-grid-4' },
				],
				angleList: [
					{ id: 'crop-angle-1' },
					{ id: 'crop-angle-2' },
					{ id: 'crop-angle-3' },
					{ id: 'crop-angle-4' },
				],
				/** 本地缓存的图片路径 */
				imgSrc: '',
				/** 图片的裁剪宽度 */
				imgWidth: IMG_SIZE,
				/** 图片的裁剪高度 */
				imgHeight: IMG_SIZE,
				/** 裁剪区域最大宽度所占屏幕宽度百分比 */
				widthPercent: AREA_SIZE,
				/** 裁剪区域最大高度所占屏幕宽度百分比 */
				heightPercent: AREA_SIZE,
				/** 裁剪区域布局信息 */
				area: {},
				/** 未被缩放过的图片宽 */
				oldWidth: 0,
				/** 未被缩放过的图片高 */
				oldHeight: 0,
				/** 系统信息 */
				sys: uni.getSystemInfoSync(),
				scaleWidth: 0,
				scaleHeight: 0,
				rotate: 0,
				offsetX: 0,
				offsetY: 0,
				use2d: false,
				canvansWidth: 0,
				canvansHeight: 0,
				// imageStyles: {},
				// maskStylesList: [{}, {}, {}, {}],
				// borderStyles: {},
				// gridStylesList: [{}, {}, {}, {}],
				// angleStylesList: [{}, {}, {}, {}],
				// circleBoxStyles: {},
				// circleStyles: {},
			}
		},
		computed: {
			initData() {
				// console.log('initData')
				return {
					timestamp: new Date().getTime(),
					area: {
						...this.area,
						bounce: this.bounce,
						showBorder: this.showBorder,
						showGrid: this.showGrid,
						showAngle: this.showAngle,
						angleSize: this.angleSize,
						angleBorderWidth: this.angleBorderWidth,
						minScale: this.areaScale,
						widthPercent: this.widthPercent,
						heightPercent: this.heightPercent,
						radius: this.radius
					},
					sys: this.sys,
					img: {
						maxScale: this.maxScale,
						src: this.imgSrc,
						width: this.oldWidth,
						height: this.oldHeight,
						oldWidth: this.oldWidth,
						oldHeight: this.oldHeight,
					}
				}
			},
			imgProps() {
				return {
					width: this.width,
					height: this.height,
					src: this.src,
				}
			}
		},
		watch: {
			imgProps: {
				handler(val) {
					// console.log('imgProps', val)
					// 自定义裁剪尺，示例如下：
					this.imgWidth = Number(val.width) || IMG_SIZE;
					this.imgHeight = Number(val.height) || IMG_SIZE;
					let use2d = true;
					// #ifndef MP-WEIXIN
					use2d = false;
					// #endif
					// if(use2d && (this.imgWidth > 1365 || this.imgHeight > 1365)) {
					// 	use2d = false;
					// }
					let canvansWidth = this.imgWidth;
					let canvansHeight = this.imgHeight;
					let size = Math.max(canvansWidth, canvansHeight)
					let scalc = 1;
					if(size > 1365) {
						scalc = 1365 / size;
					}
					this.canvansWidth = canvansWidth * scalc;
					this.canvansHeight = canvansHeight * scalc;
					this.use2d = use2d;
					this.initArea();
					val.src && this.initImage(val.src);
				},
				immediate: true
			},
		},
		mounted() {
			this.resetData();
			this.initImage(this.src);
		},
		methods: {
			/** 提供给wxs调用，用来接收图片变更数据 */
			dataChange(e) {
				// console.log('dataChange', e)
				this.scaleWidth = e.width;
				this.scaleHeight = e.height;
				this.rotate = e.rotate;
				this.offsetX = e.x;
				this.offsetY = e.y;
			},
			/** 初始化裁剪区域布局信息 */
			initArea() {
				// 底部操作栏高度 = 底部底部操作栏内容高度 + 设备底部安全区域高度
				this.sys.offsetBottom = uni.upx2px(100) + this.sys.safeAreaInsets.bottom;
				// #ifndef H5
				this.sys.windowTop = 0;
				this.sys.navigation = true;
				// #endif
				// #ifdef H5
				// h5平台的窗口高度是包含标题栏的
				this.sys.windowTop = this.sys.windowTop || 44;
				this.sys.navigation = this.navigation;
				// #endif
				let wp = this.widthPercent;
				let hp = this.heightPercent;
				if (this.imgWidth > this.imgHeight) {
					hp = hp * this.imgHeight / this.imgWidth;
				} else if (this.imgWidth < this.imgHeight) {
					wp = wp * this.imgWidth / this.imgHeight;
				}
				const size = this.sys.windowWidth > this.sys.windowHeight ? this.sys.windowHeight : this.sys.windowWidth;
				const width = size * wp / 100;
				const height = size * hp / 100;
				const left = (this.sys.windowWidth - width) / 2;
				const right = left + width;
				const top = (this.sys.windowHeight + this.sys.windowTop - this.sys.offsetBottom - height) / 2;
				const bottom = this.sys.windowHeight + this.sys.windowTop - this.sys.offsetBottom - top;
				this.area = { width, height, left, right, top, bottom };
				this.scaleWidth = width;
				this.scaleHeight = height;
			},
			/** 从本地选取图片 */
			chooseImage() {
				// #ifdef MP-WEIXIN || MP-JD
				if(uni.chooseMedia) {
					uni.chooseMedia({
						count: 1,
						mediaType: ['image'],
						success: (res) => {
							this.resetData();
							this.initImage(res.tempFiles[0].tempFilePath);
						}
					});
					return;
				}
				// #endif
				uni.chooseImage({
					count: 1,
					success: (res) => {
						this.resetData();
						this.initImage(res.tempFiles[0].path);
					}
				});
			},
			/** 重置数据 */
			resetData() {
				this.imgSrc = '';
				this.rotate = 0;
				this.offsetX = 0;
				this.offsetY = 0;
				this.initArea();
			},
			/**
			 * 初始化图片信息
			 * @param {String} url 图片链接
			 */
			initImage(url) {
				uni.getImageInfo({
					src: url,
					success: (res) => {
						this.imgSrc = res.path;
						let scale = res.width / res.height;
						let areaScale = this.area.width / this.area.height;
						if (scale > 1) { // 横向图片
							if (scale >= areaScale) { // 图片宽不小于目标宽，则高固定，宽自适应
								this.scaleWidth = (this.scaleHeight / res.height) * this.scaleWidth * (res.width / this.scaleWidth);
							} else { // 否则宽固定、高自适应
								this.scaleHeight = res.height * this.scaleWidth / res.width;
							}
						} else { // 纵向图片
							if (scale <= areaScale) { // 图片高不小于目标高，宽固定，高自适应
								this.scaleHeight = (this.scaleWidth / res.width) * this.scaleHeight / (this.scaleHeight / res.height);
							} else { // 否则高固定，宽自适应
								this.scaleWidth = res.width * this.scaleHeight / res.height;
							}
						}
						// 记录原始宽高，为缩放比列做限制
						this.oldWidth = this.scaleWidth;
						this.oldHeight = this.scaleHeight;
					},
					fail: (err) => {
						console.error(err)
					}
				});
			},
			/**
			 * 剪切图片圆角
			 * @param {Object} ctx canvas 的绘图上下文对象
			 * @param {Number} radius 圆角半径
			 * @param {Number} scale 生成图片的实际尺寸与截取区域比
			 * @param {Function} drawImage 执行剪切时所调用的绘图方法，入参为是否执行了剪切
			 */
			drawClipImage(ctx, radius, scale, drawImage) {
				if(radius > 0) {
					ctx.save();
					ctx.beginPath();
					const w = this.canvansWidth;
					const h = this.canvansHeight;
					if(w === h && radius >= w / 2) { // 圆形
						ctx.arc(w / 2, h / 2, w / 2, 0, 2 * Math.PI);
					} else { // 圆角矩形
						if(w !== h) { // 限制圆角半径不能超过短边的一半
							radius = Math.min(w / 2, h / 2, radius);
							// radius = Math.min(Math.max(w, h) / 2, radius);
						}
						ctx.moveTo(radius, 0);
						ctx.arcTo(w, 0, w, h, radius);
						ctx.arcTo(w, h, 0, h, radius);
						ctx.arcTo(0, h, 0, 0, radius);
						ctx.arcTo(0, 0, w, 0, radius);
						ctx.closePath();
					}
					ctx.clip();
					drawImage && drawImage(true);
					ctx.restore();
				} else {
					drawImage && drawImage(false);
				}
			},
			/**
			 * 旋转图片
			 * @param {Object} ctx canvas 的绘图上下文对象
			 * @param {Number} rotate 旋转角度
			 * @param {Number} scale 生成图片的实际尺寸与截取区域比
			 */
			drawRotateImage(ctx, rotate, scale) {
				if(rotate !== 0) {
					// 1. 以图片中心点为旋转中心点
					const x = this.scaleWidth * scale / 2;
					const y = this.scaleHeight * scale / 2;
					ctx.translate(x, y);
					// 2. 旋转画布
					ctx.rotate(rotate * Math.PI / 180);
					// 3. 旋转完画布后恢复设置旋转中心时所做的偏移
					ctx.translate(-x, -y);
				}
			},
			drawImage(ctx, image, callback) {
				// 生成图片的实际尺寸与截取区域比
				const scale = this.canvansWidth / this.area.width;
				this.drawClipImage(ctx, this.radius, scale, () => {
					this.drawRotateImage(ctx, this.rotate, scale);
					const r = this.rotate / 90;
					ctx.drawImage(
						image,
						[
							(this.offsetX - this.area.left),
							(this.offsetY - this.area.top),
							-(this.offsetX - this.area.left),
							-(this.offsetY - this.area.top)
						][r] * scale,
						[
							(this.offsetY - this.area.top),
							-(this.offsetX - this.area.left),
							-(this.offsetY - this.area.top),
							(this.offsetX - this.area.left)
						][r] * scale,
						this.scaleWidth * scale,
						this.scaleHeight * scale
					);
				});
			},
			/**
			 * 绘图
			 * @param {Object} canvas 
			 * @param {Object} ctx canvas 的绘图上下文对象
			 * @param {String} src 图片路径
			 * @param {Function} callback 开始绘制时回调
			 */
			draw2DImage(canvas, ctx, src, callback) {
				// console.log('draw2DImage', canvas, ctx, src, callback)
				if(canvas) {
					const image = canvas.createImage();
					image.onload = () => {
						this.drawImage(ctx, image);
						// 如果觉得`生成时间过长`或`出现生成图片空白`可尝试调整延迟时间
						callback && setTimeout(callback, this.delay);
					};
					image.onerror = (err) => {
						console.error(err)
						uni.hideLoading();
					};
					image.src = src;
				} else {
					this.drawImage(ctx, src);
					setTimeout(() => {
						ctx.draw(false, callback);
					}, 200);
				}
			},
			/**
			 * 画布转图片到本地缓存
			 * @param {Object} canvas 
			 * @param {String} canvasId 
			 */
			canvasToTempFilePath(canvas, canvasId) {
				// console.log('canvasToTempFilePath', canvas, canvasId)
				uni.canvasToTempFilePath({
					canvas,
					canvasId,
					x: 0,
					y: 0,
					width: this.canvansWidth,
					height: this.canvansHeight,
					destWidth: this.imgWidth, // 必要，保证生成图片宽度不受设备分辨率影响
					destHeight: this.imgHeight, // 必要，保证生成图片高度不受设备分辨率影响
					fileType: this.fileType, // 目标文件的类型，默认png
					success: (res) => {
						// 生成的图片临时文件路径
						this.handleImage(res.tempFilePath);
					},
					fail: (err) => {
						uni.hideLoading();
						uni.showToast({ title: '裁剪失败，生成图片异常！', icon: 'none' });
					}
				}, this);
			},
			/** 确认裁剪 */
			cropClick() {
				uni.showLoading({ title: '裁剪中...', mask: true });
				if(!this.use2d) {
					const ctx = uni.createCanvasContext('imgCanvas', this);
					ctx.clearRect(0, 0, this.canvansWidth, this.canvansHeight);
					this.draw2DImage(null, ctx, this.imgSrc, () => {
						this.canvasToTempFilePath(null, 'imgCanvas');
					});
					return;
				}
				// #ifdef MP-WEIXIN
				const query = uni.createSelectorQuery().in(this);
				query.select('#imgCanvas')
					.fields({ node: true, size: true })
					.exec((res) => {
						const canvas = res[0].node;
										
						const dpr = uni.getSystemInfoSync().pixelRatio;
						canvas.width = res[0].width * dpr;
						canvas.height = res[0].height * dpr;
						const ctx = canvas.getContext('2d');
						ctx.scale(dpr, dpr);
						ctx.clearRect(0, 0, this.canvansWidth, this.canvansHeight);
						
						this.draw2DImage(canvas, ctx, this.imgSrc, () => {
							this.canvasToTempFilePath(canvas);
						});
					});
				// #endif
			},
			handleImage(tempFilePath){
				// 在H5平台下，tempFilePath 为 base64
				// console.log(tempFilePath)
				uni.hideLoading();
				this.$emit('crop', { tempFilePath });
			}
		}
	}
</script>
 
<style lang="scss" scoped>
	.crop-border {
		position: fixed;
		border: 2rpx solid #fff;
		box-sizing: border-box;
		z-index: 100;
		pointer-events: none;
	}
	.image-cropper {
		position: fixed;
		left: 0;
		right: 0;
		top: 0;
		bottom: 0;
		overflow: hidden;
		display: flex;
		flex-direction: column;
		background-color: #000;
		.img-canvas {
			position: absolute !important;
			transform: translateX(-100%);
		}
		.pic-preview {
			width: 100%;
			flex: 1;
			position: relative;

			.crop-mask-block {
				background-color: rgba(51, 51, 51, 0.8);
				z-index: 2;
				position: fixed;
				box-sizing: border-box;
				pointer-events: none;
			}
			.crop-circle-box {
				position: fixed;
				box-sizing: border-box;
				z-index: 2;
				pointer-events: none;
				overflow: hidden;
				.crop-circle {
					width: 100%;
					height: 100%;
				}
			}
			.crop-image {
				padding: 0 !important;
				margin: 0 !important;
				border-radius: 0 !important;
				display: block !important;
			}

			.crop-grid {
				position: fixed;
				z-index: 3;
				border-style: dashed;
				border-color: #fff;
				pointer-events: none;
				opacity: 0.5;
			}
			.crop-angle {
				position: fixed;
				z-index: 3;
				border-style: solid;
				border-color: #fff;
				pointer-events: none;
			}
		}

		.fixed-bottom {
			position: fixed;
			left: 0;
			right: 0;
			bottom: 20rpx;
			z-index: 99;

			.rotate-icon {
				background-image: url('');
				background-size: 60% 60%;
				background-repeat: no-repeat;
				background-position:center;
				width: 80rpx;
				height: 80rpx;
				position: absolute;
				top: -90rpx;
				left: 10rpx;
				transform: rotateY(180deg);
			}

			.rechoose {
        display: inline-block;
        vertical-align: top;
        margin-top: 8rpx;
        color: #F8F8F8;
			}

			.choose-btn {
				color: $uni-color-primary;
				text-align: center;
				line-height: 100rpx;
				flex: 1;
			}

			.button {
				color: #fff;
        background-color: #000000;
        border:1rpx solid #FFFFFF;
        vertical-align: top;
			}
		}

		.safe-area-inset-bottom {
			padding-bottom: 0;  
			padding-bottom: constant(safe-area-inset-bottom); // 兼容 IOS<11.2
			padding-bottom: env(safe-area-inset-bottom); // 兼容 IOS>=11.2
		}
 
	}
</style>